/*
 * Copyright (c) 2001-2006 Caucho Technology, Inc.  All rights reserved.
 *
 * The Apache Software License, Version 1.1
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Caucho Technology (http://www.caucho.com/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "Hessian", "Resin", and "Caucho" must not be used to
 *    endorse or promote products derived from this software without prior
 *    written permission. For written permission, please contact
 *    info@caucho.com.
 *
 * 5. Products derived from this software may not be called "Resin"
 *    nor may "Resin" appear in their names without prior written
 *    permission of Caucho Technology.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL CAUCHO TECHNOLOGY OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Scott Ferguson
 */

package com.caucho.burlap.io;

import com.caucho.hessian.io.Deserializer;
import com.caucho.hessian.io.HessianRemoteResolver;
import com.caucho.hessian.io.SerializerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.TimeZone;

/**
 * Input stream for Burlap requests.
 *
 * <p>BurlapInput is unbuffered, so any client needs to provide
 * its own buffering.
 *
 * <pre>
 * InputStream is = ...; // from http connection
 * BurlapInput in = new BurlapInput(is);
 * String value;
 *
 * in.startReply();         // read reply header
 * value = in.readString(); // read string value
 * in.completeReply();      // read reply footer
 * </pre>
 */
public class BurlapInput extends AbstractBurlapInput {
    private static int[]        base64Decode;

    public final static int     TAG_EOF         = -1;

    public final static int     TAG_NULL        = 0;
    public final static int     TAG_BOOLEAN     = 1;
    public final static int     TAG_INT         = 2;
    public final static int     TAG_LONG        = 3;
    public final static int     TAG_DOUBLE      = 4;
    public final static int     TAG_DATE        = 5;
    public final static int     TAG_STRING      = 6;
    public final static int     TAG_XML         = 7;
    public final static int     TAG_BASE64      = 8;
    public final static int     TAG_MAP         = 9;
    public final static int     TAG_LIST        = 10;
    public final static int     TAG_TYPE        = 11;
    public final static int     TAG_LENGTH      = 12;

    public final static int     TAG_REF         = 13;
    public final static int     TAG_REMOTE      = 14;

    public final static int     TAG_CALL        = 15;
    public final static int     TAG_REPLY       = 16;
    public final static int     TAG_FAULT       = 17;
    public final static int     TAG_METHOD      = 18;
    public final static int     TAG_HEADER      = 19;

    public final static int     TAG_NULL_END    = TAG_NULL + 100;
    public final static int     TAG_BOOLEAN_END = TAG_BOOLEAN + 100;
    public final static int     TAG_INT_END     = TAG_INT + 100;
    public final static int     TAG_LONG_END    = TAG_LONG + 100;
    public final static int     TAG_DOUBLE_END  = TAG_DOUBLE + 100;
    public final static int     TAG_DATE_END    = TAG_DATE + 100;
    public final static int     TAG_STRING_END  = TAG_STRING + 100;
    public final static int     TAG_XML_END     = TAG_XML + 100;
    public final static int     TAG_BASE64_END  = TAG_BASE64 + 100;
    public final static int     TAG_MAP_END     = TAG_MAP + 100;
    public final static int     TAG_LIST_END    = TAG_LIST + 100;
    public final static int     TAG_TYPE_END    = TAG_TYPE + 100;
    public final static int     TAG_LENGTH_END  = TAG_LENGTH + 100;

    public final static int     TAG_REF_END     = TAG_REF + 100;
    public final static int     TAG_REMOTE_END  = TAG_REMOTE + 100;

    public final static int     TAG_CALL_END    = TAG_CALL + 100;
    public final static int     TAG_REPLY_END   = TAG_REPLY + 100;
    public final static int     TAG_FAULT_END   = TAG_FAULT + 100;
    public final static int     TAG_METHOD_END  = TAG_METHOD + 100;
    public final static int     TAG_HEADER_END  = TAG_HEADER + 100;

    private static HashMap      _tagMap;

    private static Field        _detailMessageField;

    protected SerializerFactory _serializerFactory;

    protected ArrayList         _refs;

    // the underlying input stream
    private InputStream         _is;
    // a peek character
    protected int               _peek           = -1;

    // the method for a call
    private String              _method;

    private int                 _peekTag;

    private Throwable           _replyFault;

    protected StringBuffer      _sbuf           = new StringBuffer();
    protected StringBuffer      _entityBuffer   = new StringBuffer();

    protected Calendar          _utcCalendar;
    protected Calendar          _localCalendar;

    /**
     * Creates an uninitialized Burlap input stream.
     */
    public BurlapInput()
    {
    }

    /**
     * Creates a new Burlap input stream, initialized with an
     * underlying input stream.
     *
     * @param is the underlying input stream.
     */
    public BurlapInput(InputStream is)
    {
        init(is);
    }

    /**
     * Sets the serializer factory.
     */
    public void setSerializerFactory(SerializerFactory factory)
    {
        _serializerFactory = factory;
    }

    /**
     * Gets the serializer factory.
     */
    public SerializerFactory getSerializerFactory()
    {
        return _serializerFactory;
    }

    /**
     * Initialize the burlap stream with the underlying input stream.
     */
    public void init(InputStream is)
    {
        _is = is;
        _method = null;
        _peek = -1;
        _peekTag = -1;
        _refs = null;
        _replyFault = null;

        if (_serializerFactory == null)
            _serializerFactory = new SerializerFactory();
    }

    /**
     * Returns the calls method
     */
    public String getMethod()
    {
        return _method;
    }

    /**
     * Returns any reply fault.
     */
    public Throwable getReplyFault()
    {
        return _replyFault;
    }

    /**
     * Starts reading the call
     *
     * <pre>
     * &lt;burlap:call>
     * &lt;method>method&lt;/method>
     * </pre>
     */
    public void startCall()
        throws IOException
    {
        readCall();

        while ((readHeader() != null))
            readObject();

        readMethod();
    }

    /**
     * Starts reading the call
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * &lt;burlap:call>
     * </pre>
     */
    public int readCall()
        throws IOException
    {
        expectTag(TAG_CALL);

        int major = 1;
        int minor = 0;

        return (major << 16) + minor;
    }

    /**
     * Reads the method
     *
     * <pre>
     * &lt;method>method&lt;/method>
     * </pre>
     */
    public String readMethod()
        throws IOException
    {
        expectTag(TAG_METHOD);

        _method = parseString();

        expectTag(TAG_METHOD_END);

        return _method;
    }

    /**
     * Completes reading the call
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * &lt;/burlap:call>
     * </pre>
     */
    public void completeCall()
        throws IOException
    {
        expectTag(TAG_CALL_END);
    }

    /**
     * Reads a reply as an object.
     * If the reply has a fault, throws the exception.
     */
    public Object readReply(Class expectedClass)
        throws Throwable
    {
        expectTag(TAG_REPLY);

        int tag = parseTag();

        if (tag == TAG_FAULT)
            throw prepareFault();
        else {
            _peekTag = tag;
            Object value = readObject(expectedClass);

            expectTag(TAG_REPLY_END);

            return value;
        }
    }

    /**
     * Starts reading the reply
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * &lt;burlap:reply>
     * &lt;value>
     * </pre>
     */
    public void startReply()
        throws Throwable
    {
        expectTag(TAG_REPLY);

        int tag = parseTag();
        if (tag == TAG_FAULT)
            throw prepareFault();
        else
            _peekTag = tag;
    }

    /**
     * Prepares the fault.
     */
    private Throwable prepareFault()
        throws IOException
    {
        HashMap fault = readFault();

        Object detail = fault.get("detail");
        String message = (String) fault.get("message");

        if (detail instanceof Throwable) {
            _replyFault = (Throwable) detail;

            if (message != null && _detailMessageField != null) {
                try {
                    _detailMessageField.set(_replyFault, message);
                } catch (Throwable e) {
                }
            }

            return _replyFault;
        }

        else {
            String code = (String) fault.get("code");

            _replyFault = new BurlapServiceException(message, code, detail);

            return _replyFault;
        }
    }

    /**
     * Completes reading the call
     *
     * <p>A successful completion will have a single value:
     *
     * <pre>
     * &lt;/burlap:reply>
     * </pre>
     */
    public void completeReply()
        throws IOException
    {
        expectTag(TAG_REPLY_END);
    }

    /**
     * Reads a header, returning null if there are no headers.
     *
     * <pre>
     * &lt;header>value&lt;/header>
     * </pre>
     */
    public String readHeader()
        throws IOException
    {
        int tag = parseTag();

        if (tag == TAG_HEADER) {
            _sbuf.setLength(0);
            String value = parseString(_sbuf).toString();
            expectTag(TAG_HEADER_END);
            return value;
        }

        _peekTag = tag;

        return null;
    }

    /**
     * Reads a null
     *
     * <pre>
     * &lt;null>&lt;/null>
     * </pre>
     */
    public void readNull()
        throws IOException
    {
        int tag = parseTag();

        switch (tag) {
            case TAG_NULL:
                expectTag(TAG_NULL_END);
                return;

            default:
                throw expectedTag("null", tag);
        }
    }

    /**
     * Reads a boolean
     *
     * <pre>
     * &lt;boolean>0&lt;/boolean>
     * &lt;boolean>1&lt;/boolean>
     * </pre>
     */
    public boolean readBoolean()
        throws IOException
    {
        int tag = parseTag();

        boolean value;

        switch (tag) {
            case TAG_NULL:
                value = false;
                expectTag(TAG_NULL_END);
                return value;

            case TAG_BOOLEAN:
                value = parseInt() != 0;
                expectTag(TAG_BOOLEAN_END);
                return value;

            case TAG_INT:
                value = parseInt() != 0;
                expectTag(TAG_INT_END);
                return value;

            case TAG_LONG:
                value = parseLong() != 0;
                expectTag(TAG_LONG_END);
                return value;

            case TAG_DOUBLE:
                value = parseDouble() != 0;
                expectTag(TAG_DOUBLE_END);
                return value;

            default:
                throw expectedTag("boolean", tag);
        }
    }

    /**
     * Reads a byte
     *
     * <pre>
     * &lt;int>value&lt;/int>
     * </pre>
     */
    public byte readByte()
        throws IOException
    {
        return (byte) readInt();
    }

    /**
     * Reads a short
     *
     * <pre>
     * &lt;int>value&lt;/int>
     * </pre>
     */
    public short readShort()
        throws IOException
    {
        return (short) readInt();
    }

    /**
     * Reads an integer
     *
     * <pre>
     * &lt;int>value&lt;/int>
     * </pre>
     */
    public int readInt()
        throws IOException
    {
        int tag = parseTag();

        int value;

        switch (tag) {
            case TAG_NULL:
                value = 0;
                expectTag(TAG_NULL_END);
                return value;

            case TAG_BOOLEAN:
                value = parseInt();
                expectTag(TAG_BOOLEAN_END);
                return value;

            case TAG_INT:
                value = parseInt();
                expectTag(TAG_INT_END);
                return value;

            case TAG_LONG:
                value = (int) parseLong();
                expectTag(TAG_LONG_END);
                return value;

            case TAG_DOUBLE:
                value = (int) parseDouble();
                expectTag(TAG_DOUBLE_END);
                return value;

            default:
                throw expectedTag("int", tag);
        }
    }

    /**
     * Reads a long
     *
     * <pre>
     * &lt;long>value&lt;/long>
     * </pre>
     */
    public long readLong()
        throws IOException
    {
        int tag = parseTag();

        long value;

        switch (tag) {
            case TAG_NULL:
                value = 0;
                expectTag(TAG_NULL_END);
                return value;

            case TAG_BOOLEAN:
                value = parseInt();
                expectTag(TAG_BOOLEAN_END);
                return value;

            case TAG_INT:
                value = parseInt();
                expectTag(TAG_INT_END);
                return value;

            case TAG_LONG:
                value = parseLong();
                expectTag(TAG_LONG_END);
                return value;

            case TAG_DOUBLE:
                value = (long) parseDouble();
                expectTag(TAG_DOUBLE_END);
                return value;

            default:
                throw expectedTag("long", tag);
        }
    }

    /**
     * Reads a float
     *
     * <pre>
     * &lt;double>value&lt;/double>
     * </pre>
     */
    public float readFloat()
        throws IOException
    {
        return (float) readDouble();
    }

    /**
     * Reads a double
     *
     * <pre>
     * &lt;double>value&lt;/double>
     * </pre>
     */
    public double readDouble()
        throws IOException
    {
        int tag = parseTag();

        double value;

        switch (tag) {
            case TAG_NULL:
                value = 0;
                expectTag(TAG_NULL_END);
                return value;

            case TAG_BOOLEAN:
                value = parseInt();
                expectTag(TAG_BOOLEAN_END);
                return value;

            case TAG_INT:
                value = parseInt();
                expectTag(TAG_INT_END);
                return value;

            case TAG_LONG:
                value = parseLong();
                expectTag(TAG_LONG_END);
                return value;

            case TAG_DOUBLE:
                value = parseDouble();
                expectTag(TAG_DOUBLE_END);
                return value;

            default:
                throw expectedTag("double", tag);
        }
    }

    /**
     * Reads a date.
     *
     * <pre>
     * &lt;date>ISO-8609 date&lt;/date>
     * </pre>
     */
    public long readUTCDate()
        throws IOException
    {
        int tag = parseTag();

        if (tag != TAG_DATE)
            throw error("expected date");

        if (_utcCalendar == null)
            _utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

        long value = parseDate(_utcCalendar);

        expectTag(TAG_DATE_END);

        return value;
    }

    /**
     * Reads a date.
     *
     * <pre>
     * &lt;date>ISO-8609 date&lt;/date>
     * </pre>
     */
    public long readLocalDate()
        throws IOException
    {
        int tag = parseTag();

        if (tag != TAG_DATE)
            throw error("expected date");

        if (_localCalendar == null)
            _localCalendar = Calendar.getInstance();

        long value = parseDate(_localCalendar);

        expectTag(TAG_DATE_END);

        return value;
    }

    /**
     * Reads a string
     *
     * <pre>
     * &lt;string>value&lt;/string>
     * </pre>
     */
    public String readString()
        throws IOException
    {
        int tag = parseTag();

        String value;

        switch (tag) {
            case TAG_NULL:
                expectTag(TAG_NULL_END);
                return null;

            case TAG_STRING:
                _sbuf.setLength(0);
                value = parseString(_sbuf).toString();
                expectTag(TAG_STRING_END);
                return value;

            case TAG_XML:
                _sbuf.setLength(0);
                value = parseString(_sbuf).toString();
                expectTag(TAG_XML_END);
                return value;

            default:
                throw expectedTag("string", tag);
        }
    }

    /**
     * Reads an XML node.
     *
     * <pre>
     * &xml;xml string&lt;/xml>
     * </pre>
     */
    public org.w3c.dom.Node readNode()
        throws IOException
    {
        int tag = read();

        switch (tag) {
            case 'N':
                return null;

            case 'S':
            case 's':
            case 'X':
            case 'x':
                throw error("can't cope");

            default:
                throw expectedTag("string", tag);
        }
    }

    /**
     * Reads a byte array
     *
     * <pre>
     * &lt;base64>...&lt;/base64>
     * </pre>
     */
    public byte[] readBytes()
        throws IOException
    {
        int tag = parseTag();

        switch (tag) {
            case TAG_NULL:
                expectTag(TAG_NULL_END);
                return null;

            case TAG_BASE64:
                byte[] data = parseBytes();
                expectTag(TAG_BASE64_END);

                return data;

            default:
                throw expectedTag("bytes", tag);
        }
    }

    /**
     * Reads a length
     *
     * <pre>
     * &lt;length>value&lt;/length>
     * </pre>
     */
    public int readLength()
        throws IOException
    {
        int tag = parseTag();

        if (tag != TAG_LENGTH) {
            _peekTag = tag;
            return -1;
        }

        int value = parseInt();

        expectTag(TAG_LENGTH_END);

        return value;
    }

    /**
     * Reads a fault.
     */
    private HashMap readFault()
        throws IOException
    {
        HashMap map = new HashMap();

        int code = parseTag();
        for (; code >= 0 && code != TAG_FAULT_END; code = parseTag()) {
            _peekTag = code;

            Object key = readObject();
            Object value = readObject();

            if (key != null && value != null)
                map.put(key, value);
        }

        if (code != TAG_FAULT_END)
            throw expectedTag("fault", code);

        return map;
    }

    /**
     * Reads an object from the input stream with an expected type.
     */
    public Object readObject(Class cl)
        throws IOException
    {
        if (cl == null || cl.equals(Object.class))
            return readObject();

        int tag = parseTag();

        switch (tag) {
            case TAG_NULL:
                expectTag(TAG_NULL_END);
                return null;

            case TAG_MAP: {
                String type = readType();
                Deserializer reader;
                reader = _serializerFactory.getObjectDeserializer(type);

                if (cl != reader.getType() && cl.isAssignableFrom(reader.getType()))
                    return reader.readMap(this);

                reader = _serializerFactory.getDeserializer(cl);

                return reader.readMap(this);
            }

            case TAG_LIST: {
                String type = readType();
                int length = readLength();

                Deserializer reader;
                reader = _serializerFactory.getObjectDeserializer(type);

                if (cl != reader.getType() && cl.isAssignableFrom(reader.getType()))
                    return reader.readList(this, length);

                reader = _serializerFactory.getDeserializer(cl);

                return reader.readList(this, length);
            }

            case TAG_REF: {
                int ref = parseInt();

                expectTag(TAG_REF_END);

                return _refs.get(ref);
            }

            case TAG_REMOTE: {
                String type = readType();
                String url = readString();

                expectTag(TAG_REMOTE_END);

                Object remote = resolveRemote(type, url);

                return remote;
            }
        }

        _peekTag = tag;

        Object value = _serializerFactory.getDeserializer(cl).readObject(this);

        return value;
    }

    /**
     * Reads an arbitrary object from the input stream when the type
     * is unknown.
     */
    public Object readObject()
        throws IOException
    {
        int tag = parseTag();

        switch (tag) {
            case TAG_NULL:
                expectTag(TAG_NULL_END);
                return null;

            case TAG_BOOLEAN: {
                int value = parseInt();
                expectTag(TAG_BOOLEAN_END);
                return new Boolean(value != 0);
            }

            case TAG_INT: {
                int value = parseInt();
                expectTag(TAG_INT_END);
                return new Integer(value);
            }

            case TAG_LONG: {
                long value = parseLong();
                expectTag(TAG_LONG_END);
                return new Long(value);
            }

            case TAG_DOUBLE: {
                double value = parseDouble();
                expectTag(TAG_DOUBLE_END);
                return new Double(value);
            }

            case TAG_DATE: {
                long value = parseDate();
                expectTag(TAG_DATE_END);
                return new Date(value);
            }

            case TAG_XML: {
                return parseXML();
            }

            case TAG_STRING: {
                _sbuf.setLength(0);

                String value = parseString(_sbuf).toString();

                expectTag(TAG_STRING_END);

                return value;
            }

            case TAG_BASE64: {
                byte[] data = parseBytes();

                expectTag(TAG_BASE64_END);

                return data;
            }

            case TAG_LIST: {
                String type = readType();
                int length = readLength();

                return _serializerFactory.readList(this, length, type);
            }

            case TAG_MAP: {
                String type = readType();
                Deserializer deserializer;
                deserializer = _serializerFactory.getObjectDeserializer(type);

                return deserializer.readMap(this);
            }

            case TAG_REF: {
                int ref = parseInt();

                expectTag(TAG_REF_END);

                return _refs.get(ref);
            }

            case TAG_REMOTE: {
                String type = readType();
                String url = readString();

                expectTag(TAG_REMOTE_END);

                return resolveRemote(type, url);
            }

            default:
                throw error("unknown code:" + tagName(tag));
        }
    }

    /**
     * Reads a remote object.
     */
    public Object readRemote()
        throws IOException
    {
        String type = readType();
        String url = readString();

        return resolveRemote(type, url);
    }

    /**
     * Reads a reference.
     */
    public Object readRef()
        throws IOException
    {
        return _refs.get(parseInt());
    }

    /**
     * Reads the start of a list.
     */
    public int readListStart()
        throws IOException
    {
        return parseTag();
    }

    /**
     * Reads the start of a map.
     */
    public int readMapStart()
        throws IOException
    {
        return parseTag();
    }

    /**
     * Returns true if this is the end of a list or a map.
     */
    public boolean isEnd()
        throws IOException
    {
        int code = parseTag();

        _peekTag = code;

        return (code < 0 || code >= 100);
    }

    /**
     * Reads the end byte.
     */
    public void readEnd()
        throws IOException
    {
        int code = parseTag();

        if (code < 100)
            throw error("unknown code:" + (char) code);
    }

    /**
     * Reads the end of the map
     */
    public void readMapEnd()
        throws IOException
    {
        expectTag(TAG_MAP_END);
    }

    /**
     * Reads the end of the map
     */
    public void readListEnd()
        throws IOException
    {
        expectTag(TAG_LIST_END);
    }

    /**
     * Adds a list/map reference.
     */
    public int addRef(Object ref)
    {
        if (_refs == null)
            _refs = new ArrayList();

        _refs.add(ref);

        return _refs.size() - 1;
    }

    /**
     * Adds a list/map reference.
     */
    public void setRef(int i, Object ref)
    {
        _refs.set(i, ref);
    }

    /**
     * Resolves a remote object.
     */
    public Object resolveRemote(String type, String url)
        throws IOException
    {
        HessianRemoteResolver resolver = getRemoteResolver();

        if (resolver != null)
            return resolver.lookup(type, url);
        else
            return new BurlapRemote(type, url);
    }

    /**
     * Parses a type from the stream.
     *
     * <pre>
     * &lt;type>type&lt;/type>
     * </pre>
     */
    public String readType()
        throws IOException
    {
        int code = parseTag();

        if (code != TAG_TYPE) {
            _peekTag = code;
            return "";
        }

        _sbuf.setLength(0);
        int ch;
        while ((ch = readChar()) >= 0)
            _sbuf.append((char) ch);
        String type = _sbuf.toString();

        expectTag(TAG_TYPE_END);

        return type;
    }

    /**
     * Parses a 32-bit integer value from the stream.
     */
    private int parseInt()
        throws IOException
    {
        int sign = 1;

        int ch = read();
        if (ch == '-') {
            sign = -1;
            ch = read();
        }

        int value = 0;
        for (; ch >= '0' && ch <= '9'; ch = read())
            value = 10 * value + ch - '0';

        _peek = ch;

        return sign * value;
    }

    /**
     * Parses a 64-bit long value from the stream.
     */
    private long parseLong()
        throws IOException
    {
        int sign = 1;

        int ch = read();
        if (ch == '-') {
            sign = -1;
            ch = read();
        }

        long value = 0;
        for (; ch >= '0' && ch <= '9'; ch = read())
            value = 10 * value + ch - '0';

        _peek = ch;

        return sign * value;
    }

    /**
     * Parses a 64-bit double value from the stream.
     *
     * <pre>
     * b64 b56 b48 b40 b32 b24 b16 b8
     * </pre>
     */
    private double parseDouble()
        throws IOException
    {
        int ch = skipWhitespace();

        _sbuf.setLength(0);

        for (; !isWhitespace(ch) && ch != '<'; ch = read())
            _sbuf.append((char) ch);

        _peek = ch;

        return new Double(_sbuf.toString()).doubleValue();
    }

    /**
     * Parses a date value from the stream.
     */
    protected long parseDate()
        throws IOException
    {
        if (_utcCalendar == null)
            _utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

        return parseDate(_utcCalendar);
    }

    /**
     * Parses a date value from the stream.
     */
    protected long parseDate(Calendar calendar)
        throws IOException
    {
        int ch = skipWhitespace();

        int year = 0;
        for (int i = 0; i < 4; i++) {
            if (ch >= '0' && ch <= '9')
                year = 10 * year + ch - '0';
            else
                throw expectedChar("year", ch);

            ch = read();
        }

        int month = 0;
        for (int i = 0; i < 2; i++) {
            if (ch >= '0' && ch <= '9')
                month = 10 * month + ch - '0';
            else
                throw expectedChar("month", ch);

            ch = read();
        }

        int day = 0;
        for (int i = 0; i < 2; i++) {
            if (ch >= '0' && ch <= '9')
                day = 10 * day + ch - '0';
            else
                throw expectedChar("day", ch);

            ch = read();
        }

        if (ch != 'T')
            throw expectedChar("`T'", ch);

        ch = read();

        int hour = 0;
        for (int i = 0; i < 2; i++) {
            if (ch >= '0' && ch <= '9')
                hour = 10 * hour + ch - '0';
            else
                throw expectedChar("hour", ch);

            ch = read();
        }

        int minute = 0;
        for (int i = 0; i < 2; i++) {
            if (ch >= '0' && ch <= '9')
                minute = 10 * minute + ch - '0';
            else
                throw expectedChar("minute", ch);

            ch = read();
        }

        int second = 0;
        for (int i = 0; i < 2; i++) {
            if (ch >= '0' && ch <= '9')
                second = 10 * second + ch - '0';
            else
                throw expectedChar("second", ch);

            ch = read();
        }

        int ms = 0;
        if (ch == '.') {
            ch = read();

            while (ch >= '0' && ch <= '9') {
                ms = 10 * ms + ch - '0';

                ch = read();
            }
        }

        for (; ch > 0 && ch != '<'; ch = read()) {
        }

        _peek = ch;

        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, month - 1);
        calendar.set(Calendar.DAY_OF_MONTH, day);
        calendar.set(Calendar.HOUR_OF_DAY, hour);
        calendar.set(Calendar.MINUTE, minute);
        calendar.set(Calendar.SECOND, second);
        calendar.set(Calendar.MILLISECOND, ms);

        return calendar.getTime().getTime();
    }

    protected String parseString()
        throws IOException
    {
        _sbuf.setLength(0);

        return parseString(_sbuf).toString();
    }

    /**
     * Parses a string value from the stream.  The burlap object's
     * string buffer is used for the result.
     */
    protected StringBuffer parseString(StringBuffer sbuf)
        throws IOException
    {
        int ch;

        while ((ch = readChar()) >= 0)
            sbuf.append((char) ch);

        return sbuf;
    }

    org.w3c.dom.Node parseXML()
        throws IOException
    {
        throw error("help!");
    }

    /**
     * Reads a character from the underlying stream.
     */
    int readChar()
        throws IOException
    {
        int ch = read();

        if (ch == '<' || ch < 0) {
            _peek = ch;
            return -1;
        }

        if (ch == '&') {
            ch = read();

            if (ch == '#') {
                ch = read();

                if (ch >= '0' && ch <= '9') {
                    int v = 0;
                    for (; ch >= '0' && ch <= '9'; ch = read()) {
                        v = 10 * v + ch - '0';
                    }

                    if (ch != ';')
                        throw error("expected ';' at " + (char) ch);

                    return (char) v;
                }
                else
                    throw error("expected digit at " + (char) ch);
            }
            else {
                _entityBuffer.setLength(0);

                for (; ch >= 'a' && ch <= 'z'; ch = read())
                    _entityBuffer.append((char) ch);

                String entity = _entityBuffer.toString();

                if (ch != ';')
                    throw expectedChar("';'", ch);

                if (entity.equals("amp"))
                    return '&';
                else if (entity.equals("apos"))
                    return '\'';
                else if (entity.equals("quot"))
                    return '"';
                else if (entity.equals("lt"))
                    return '<';
                else if (entity.equals("gt"))
                    return '>';
                else
                    throw new BurlapProtocolException("unknown XML entity &" + entity + "; at `" + (char) ch + "'");
            }
        }
        else if (ch < 0x80)
            return (char) ch;
        else if ((ch & 0xe0) == 0xc0) {
            int ch1 = read();
            int v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);

            return (char) v;
        }
        else if ((ch & 0xf0) == 0xe0) {
            int ch1 = read();
            int ch2 = read();
            int v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);

            return (char) v;
        }
        else
            throw new BurlapProtocolException("bad utf-8 encoding");
    }

    /**
     * Parses a byte array.
     */
    protected byte[] parseBytes()
        throws IOException
    {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        parseBytes(bos);

        return bos.toByteArray();
    }

    /**
     * Parses a byte array.
     */
    protected ByteArrayOutputStream parseBytes(ByteArrayOutputStream bos)
        throws IOException
    {
        int ch;
        for (ch = skipWhitespace(); ch >= 0 && ch != '<'; ch = skipWhitespace()) {
            int b1 = ch;
            int b2 = read();
            int b3 = read();
            int b4 = read();

            if (b4 != '=') {
                int chunk = ((base64Decode[b1] << 18) +
                    (base64Decode[b2] << 12) +
                    (base64Decode[b3] << 6) +
                        (base64Decode[b4]));

                bos.write(chunk >> 16);
                bos.write(chunk >> 8);
                bos.write(chunk);
            }
            else if (b3 != '=') {
                int chunk = ((base64Decode[b1] << 10) +
                    (base64Decode[b2] << 4) +
                        (base64Decode[b3] >> 2));

                bos.write(chunk >> 8);
                bos.write(chunk);
            }
            else {
                int chunk = ((base64Decode[b1] << 2) +
                        (base64Decode[b2] >> 4));

                bos.write(chunk);
            }
        }

        if (ch == '<')
            _peek = ch;

        return bos;
    }

    public void expectTag(int expectTag)
        throws IOException
    {
        int tag = parseTag();

        if (tag != expectTag)
            throw error("expected " + tagName(expectTag) + " at " + tagName(tag));
    }

    /**
     * Parses a tag.  Returns true if it's a start tag.
     */
    protected int parseTag()
        throws IOException
    {
        if (_peekTag >= 0) {
            int tag = _peekTag;
            _peekTag = -1;
            return tag;
        }

        int ch = skipWhitespace();
        int endTagDelta = 0;

        if (ch != '<')
            throw expectedChar("'<'", ch);

        ch = read();
        if (ch == '/') {
            endTagDelta = 100;
            ch = _is.read();
        }

        if (!isTagChar(ch))
            throw expectedChar("tag", ch);

        _sbuf.setLength(0);
        for (; isTagChar(ch); ch = read())
            _sbuf.append((char) ch);

        if (ch != '>')
            throw expectedChar("'>'", ch);

        Integer value = (Integer) _tagMap.get(_sbuf.toString());
        if (value == null)
            throw error("Unknown tag <" + _sbuf + ">");

        return value.intValue() + endTagDelta;
    }

    /**
     * Returns true if the character is a valid tag character.
     */
    private boolean isTagChar(int ch)
    {
        return (ch >= 'a' && ch <= 'z' ||
            ch >= 'A' && ch <= 'Z' ||
            ch >= '0' && ch <= '9' ||
            ch == ':' || ch == '-');
    }

    protected int skipWhitespace()
        throws IOException
    {
        int ch = read();

        for (; ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; ch = read()) {
        }

        return ch;
    }

    protected boolean isWhitespace(int ch)
        throws IOException
    {
        return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
    }

    /**
     * Reads bytes from the underlying stream.
     */
    int read(byte[] buffer, int offset, int length)
        throws IOException
    {
        throw new UnsupportedOperationException();
    }

    int read()
        throws IOException
    {
        if (_peek >= 0) {
            int value = _peek;
            _peek = -1;
            return value;
        }

        int ch = _is.read();
        return ch;
    }

    public Reader getReader()
    {
        return null;
    }

    public InputStream readInputStream()
    {
        return null;
    }

    public InputStream getInputStream()
    {
        return null;
    }

    protected IOException expectBeginTag(String expect, String tag)
    {
        return new BurlapProtocolException("expected <" + expect + "> at <" + tag + ">");
    }

    protected IOException expectedChar(String expect, int ch)
    {
        if (ch < 0)
            return error("expected " + expect + " at end of file");
        else
            return error("expected " + expect + " at " + (char) ch);
    }

    protected IOException expectedTag(String expect, int tag)
    {
        return error("expected " + expect + " at " + tagName(tag));
    }

    protected IOException error(String message)
    {
        return new BurlapProtocolException(message);
    }

    protected static String tagName(int tag)
    {
        switch (tag) {
            case TAG_NULL:
                return "<null>";
            case TAG_NULL_END:
                return "</null>";

            case TAG_BOOLEAN:
                return "<boolean>";
            case TAG_BOOLEAN_END:
                return "</boolean>";

            case TAG_INT:
                return "<int>";
            case TAG_INT_END:
                return "</int>";

            case TAG_LONG:
                return "<long>";
            case TAG_LONG_END:
                return "</long>";

            case TAG_DOUBLE:
                return "<double>";
            case TAG_DOUBLE_END:
                return "</double>";

            case TAG_STRING:
                return "<string>";
            case TAG_STRING_END:
                return "</string>";

            case TAG_XML:
                return "<xml>";
            case TAG_XML_END:
                return "</xml>";

            case TAG_BASE64:
                return "<base64>";
            case TAG_BASE64_END:
                return "</base64>";

            case TAG_MAP:
                return "<map>";
            case TAG_MAP_END:
                return "</map>";

            case TAG_LIST:
                return "<list>";
            case TAG_LIST_END:
                return "</list>";

            case TAG_TYPE:
                return "<type>";
            case TAG_TYPE_END:
                return "</type>";

            case TAG_LENGTH:
                return "<length>";
            case TAG_LENGTH_END:
                return "</length>";

            case TAG_REF:
                return "<ref>";
            case TAG_REF_END:
                return "</ref>";

            case TAG_REMOTE:
                return "<remote>";
            case TAG_REMOTE_END:
                return "</remote>";

            case TAG_CALL:
                return "<burlap:call>";
            case TAG_CALL_END:
                return "</burlap:call>";

            case TAG_REPLY:
                return "<burlap:reply>";
            case TAG_REPLY_END:
                return "</burlap:reply>";

            case TAG_HEADER:
                return "<header>";
            case TAG_HEADER_END:
                return "</header>";

            case TAG_FAULT:
                return "<fault>";
            case TAG_FAULT_END:
                return "</fault>";

            case -1:
                return "end of file";

            default:
                return "unknown " + tag;
        }
    }

    static {
        _tagMap = new HashMap();
        _tagMap.put("null", new Integer(TAG_NULL));

        _tagMap.put("boolean", new Integer(TAG_BOOLEAN));
        _tagMap.put("int", new Integer(TAG_INT));
        _tagMap.put("long", new Integer(TAG_LONG));
        _tagMap.put("double", new Integer(TAG_DOUBLE));

        _tagMap.put("date", new Integer(TAG_DATE));

        _tagMap.put("string", new Integer(TAG_STRING));
        _tagMap.put("xml", new Integer(TAG_XML));
        _tagMap.put("base64", new Integer(TAG_BASE64));

        _tagMap.put("map", new Integer(TAG_MAP));
        _tagMap.put("list", new Integer(TAG_LIST));

        _tagMap.put("type", new Integer(TAG_TYPE));
        _tagMap.put("length", new Integer(TAG_LENGTH));

        _tagMap.put("ref", new Integer(TAG_REF));
        _tagMap.put("remote", new Integer(TAG_REMOTE));

        _tagMap.put("burlap:call", new Integer(TAG_CALL));
        _tagMap.put("burlap:reply", new Integer(TAG_REPLY));
        _tagMap.put("fault", new Integer(TAG_FAULT));
        _tagMap.put("method", new Integer(TAG_METHOD));
        _tagMap.put("header", new Integer(TAG_HEADER));
    }

    static {
        base64Decode = new int[256];
        for (int i = 'A'; i <= 'Z'; i++)
            base64Decode[i] = i - 'A';
        for (int i = 'a'; i <= 'z'; i++)
            base64Decode[i] = i - 'a' + 26;
        for (int i = '0'; i <= '9'; i++)
            base64Decode[i] = i - '0' + 52;
        base64Decode['+'] = 62;
        base64Decode['/'] = 63;
    }

    static {
        try {
            _detailMessageField = Throwable.class.getDeclaredField("detailMessage");
            _detailMessageField.setAccessible(true);
        } catch (Throwable e) {
        }
    }
}
